Plot Twist

Adding Interactivity to the
Elegance of ggplot2 with ggiraph

Tanya Shapiro & Cédric Scherer
useR 2025

Hi, I'm Tanya

Tanya Logo
BEFORE
Data Professional with Insurance Background
NOW
Founder, Consultant IndieVisual

Hi, I'm Cédric

Cedric Logo
BEFORE
Computational Ecologist
NOW
Independent professional in data visualization & information design

Some Background

Why we ❤️ ggplot2

  • Grammar of Graphics - intuitive and powerful
  • Layered approach - build plots step by step
  • Beautiful & customizable - flexible styling
  • Extensible ecosystem - endless possibilities & extensions
  • Community - best part, the people! #TidyTuesday

Illustration by Allison Horst

What is Interactive Data Visualization?

Static plots tell a story

Interactive plots invites people to explore the story

Use Cases for Interactive Viz

Exploration

  • Hover for details
  • Click to filter
  • Zoom and pan

Dashboards

  • Real-time updates
  • User-driven analysis
  • Multiple linked views

Storytelling

  • Guided narratives
  • Progressive disclosure
  • Engagement boost

Presentations

  • Live demonstrations
  • Audience participation
  • Memorable experiences

Where to start if you’re an R user?

  • Starting something new can feel overwhelming
  • Interactive data viz requires extensive knowledge in Javascript & D3
  • What if we could build from a familiar starting point?

The ggiraph Philosophy

“If you know ggplot2…you already know ggiraph”

-Plausible quote from Hadley Wickham

Hadley Wickham

Hadley Wickham, Father of ggplot2

ggiraph interactive geoms

Total of 50 interactive geoms! Consistent naming convention to match ggplot2 geoms

ggplot2 ggiraph
geom_point ➡️ geom_point_interactive
geom_text ➡️ geom_text_interactive
geom_line ➡️ geom_line_interactive
geom_tile ➡️ geom_tile_interactive

Why ggiraph?

Familiar syntax - Just add _interactive to your geoms

No JavaScript required - Stay in your R comfort zone

R universe- Works with Quarto, Shiny, R Markdown,

HTML widgets - Easy to embed and share

Use with other extensions - Doesn’t interefere with non-interactive geoms

Examples: Tooltips

Code - Basic Tooltip

p_simpsons <-
  p_simpsons_base +
  geom_tile_interactive(
    aes(tooltip = title, data_id = id), 
    color = "white", stroke = .2
    ) +
  geomtextpath::geom_texthline(
    yintercept = 9.5, linewidth = 1, label = "Season 10 starts", 
    vjust = 1.4, hjust = .995, family = "Rethink Sans", lineheight = .6
  )

girafe(
  ggobj = p_simpsons,
  width_svg = 10.8, height_svg = 9.5,
  options = list(
    opts_tooltip(
      opacity = 1, use_fill = TRUE,
      css = "color: black; padding: 15px;"
    ),
    opts_sizing(width = .7),
    opts_hover(css = "stroke-width: 2;"),
    opts_hover_inv(css = "opacity: 0.3;")
  )
)

Code - Advanced Tooltip

p_simpsons_base <-
  simpsons_imdb |> 
  mutate(
    title_wrapped = stringr::str_replace_all(stringr::str_wrap(title, 22), "\\n", "<br>"),
    text_color = if_else(rating > 6.3 & rating < 8.5, "black", "white"),
    lab = paste0("<span style='font-family:rethink sans;color:", text_color, ";'>", "S", 
                 sprintf("%02d", season), " E", sprintf("%02d", episode), 
                 "<br><b style='font-size:150%;font-weight:600;font-family:piazzolla;'>", 
                 title_wrapped, "</b><br><br>IMDb Rating: ", sprintf("%1.1f", rating))
  )

p_simpsons_advanced <-
  p_simpsons_base +
  geom_tile_interactive(aes(tooltip = lab, data_id = id), color = "white", stroke = .2) +
  geomtextpath::geom_texthline(
    yintercept = 9.5, linewidth = 1, label = "Season 10 starts", 
    vjust = 1.4, hjust = .995, family = "Rethink Sans", lineheight = .6
  )

Showstopper Goes Here

Embed Ced’s bike plot here

Example: Hovering & Highlighting

Code - Basic Highlight

doctor_who_basic_plot<-ggplot() +
   #interactive points per episode
   ggiraph::geom_jitter_interactive(
     data = df_eps,
     position = position_jitter(seed = 42, height = .2, width =3),
     mapping = aes(
       data_id = story_number,
       x = rating,
       y = reorder(doctor, avg_rating),
       fill = I(color),
       tooltip = tooltip
     ),
     shape = 21,
     color = "black",
     size = 3,
     alpha = 0.8
   ) +
   geomtextpath::geom_textvline(
     mapping = aes(
       xintercept = overall_avg,
       label = paste0("Overall Avg: ", round(overall_avg, 0))
     ),
     size = 3,
     color = pal_line,
     hjust = 0.86,
     vjust = -.2,
     family = "Roboto"
   ) +
   geom_segment(
     data = df_doc_avg,
     mapping = aes(
       x = avg_rating,
       xend = overall_avg,
       y = doctor,
       yend = doctor
     ),
     color = pal_line
   ) +
   geom_point(
     data = df_doc_avg,
     mapping = aes(x = avg_rating, y = doctor, fill = I(color)),
     shape = 21, 
     color = "white",
     size = 10
   ) +
   geom_image(
     data = df_doc_avg,
     mapping = aes(x = avg_rating, y = doctor, image = image),
     size = 0.06,
     asp = 1.61
   ) +
   geom_text(
     data = df_doc_avg,
     mapping = aes(
       x = avg_rating,
       y = doctor,
       label = round(avg_rating, 1)
     ),
     size = 2.5,
     fontface= "bold",
     color = "white",
     vjust = 3.75,
     family = "Roboto"
   ) +
   geom_textbox(
     data = df_doc_avg,
     mapping = aes(x = 59.1, y = doctor, label = label),
     family = "Roboto",
     fill = NA,
     box.size = NA,
     box.padding = unit(rep(0, 4), "pt"),
     color = pal_text,
     hjust = 0
   ) +
   #arrows
   annotate(
     geom = "text",
     label = "Avg Rating\nper Doctor",
     x = 76,
     y = 2.5,
     size = 2.5,
     color = "white",
     family = "Roboto"
   ) +
   geom_curve(
     mapping = aes(
       x = 77,
       xend = 81.4,
       y = 2.7,
       yend = 3
     ),
     color = "white",
     curvature = -0.2,
     linewidth = 0.3,
     arrow = arrow(length = unit(0.08, "in"))
   ) +
   geom_curve(
     mapping = aes(
       x = 77,
       xend = 80.8,
       y = 2.3,
       yend = 2
     ),
     color = "white",
     curvature = 0.2,
     linewidth = 0.3,
     arrow = arrow(length = unit(0.08, "in"))
   ) +
   scale_x_continuous(
     limits = c(59, 95),
     expand = c(0, 0),
     breaks = c(70, 75, 80, 85, 90, 95)
   ) +
   coord_equal(ratio = 50 / 12) +
   labs(
     title = "Doctor Who was The Best?",
     subtitle = "Ratings by Episode and Doctor for the popular TV series, Doctor Who.",
     x = "Rating"
   )+
   theme(
     legend.position = "none",
     plot.background = element_rect(fill = pal_bg, color = pal_bg),
     panel.background = element_blank(),
     panel.grid = element_blank(),
     plot.margin = margin(
       l = 20,
       r = 40,
       b = 10,
       t = 20
     ),
     plot.caption = element_text(size = 7, color = "grey80"),
     plot.title = element_text(
       size = 14,
       face = "bold",
       margin = margin(b = 5)
     ),
     plot.subtitle  = element_text(size = 9, color = "#BABABA"),
     text = element_text(color = pal_text, family = "Roboto"),
     axis.text = element_text(color = pal_text, family = "Roboto Mono"),
     axis.text.y = element_blank(),
     axis.title.y = element_blank(),
     axis.title.x = element_textbox_simple(
       margin = margin(t = 10),
       halign = 0.675,
       hjust = 0.5
     ),
     axis.ticks = element_blank()
   )
 


 ggiraph::girafe(
   ggobj = doctor_who_basic_plot,
   options = list(
      ggiraph::opts_toolbar(saveaspng = F),
      ggiraph::opts_tooltip(css = "font-family:Roboto;"),
      #modify hover css
      ggiraph::opts_hover(css = "fill:white;stroke:grey;cursor:help;")
      )
   )

Code - Advanced Highlight

ggiraph::girafe(
   ggobj = doctor_who_advanced_plot,
   width_svg = 6.125, height_svg = 4.5,
   options = list(
     #turnoff download png
    ggiraph::opts_toolbar(saveaspng = F),
    ggiraph::opts_sizing(width = .8),
    #default tooltip font
    ggiraph::opts_tooltip(
      css = "font-family:Roboto;"
    ),
    #remove default opts_hover settings
    ggiraph::opts_hover(css=""),
    #inverted hover, color points grey
    ggiraph::opts_hover_inv(
      girafe_css(
        css = "", 
        point = "fill:#515151",
        text = NULL
      )
      )
   )
 )

…a creative use case with ggiraph hover

Example: Combo Plots

Combo Plots

Code - Combo Plot

combined_owid <- plot_owid + map_owid +
  plot_layout(ncol = 2, widths = c(.4, .6)) +
  plot_annotation(theme = theme(plot.margin = margin(12, 12, 12, 12)))

girafe(
  ggobj = combined_owid, width_svg = 12, height_svg = 5.3,
  options = list(
    opts_tooltip(use_fill = TRUE, css = "
    font-size: 17px;
    font-weight: 400;
    font-family: Spline Sans;
    color:white;
    padding: 10px;
    border:2px solid white;
    border-radius: 5px;
    "),
    opts_hover(css = "stroke: white; stroke-width: 0.5px; opacity: 1;"),
    opts_hover_inv(css = "opacity: 0.2;"),
    opts_toolbar(position = "bottomright"),
    opts_zoom(min = 1, max = 4)
  )
)

Example: Shiny

Thank You!

Screenshot of our interactive online course "ggplot2 [un]charted"

ggplot2-uncharted.com